package com.xored.glance.search.engine;

import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;

/**
 * @author Yuri Strot
 * 
 */
public class SearchEngine extends Thread {

	private Pattern pattern;
	private List<Match> matches;
	private Object monitor = new Object();
	private boolean exit;
	private boolean paused;
	private boolean prefix;

	private ISearchListener listener;
	private ISearchUnit[] blocks;
	private int startIndex;

	public SearchEngine(ISearchListener listener) {
		this.listener = listener;
	}

	private static final long PAUSE = 100 * 5;// 100 s

	public void setMatcher(Pattern pattern, boolean prefix,
			ISearchUnit[] blocks, int startIndex, boolean paused) {
		synchronized (monitor) {
			this.pattern = pattern;
			this.blocks = blocks;
			this.startIndex = startIndex;
			this.paused = paused;
			this.prefix = prefix;
			if (pattern != null)
				interrupt();
		}
	}

	public void exit() {
		synchronized (monitor) {
			exit = true;
			interrupt();
		}
	}

	private boolean isMatcherChanged() {
		synchronized (monitor) {
			return pattern != null;
		}
	}

	private void sleepWhileInterrupted(long millis) {
		try {
			if (millis == 0) {
				while (true)
					Thread.sleep(50);
			} else
				Thread.sleep(millis);
		} catch (InterruptedException e1) {
		}
	}

	public void run() {
		while (!exit) {
			sleepWhileInterrupted(0);
			if (exit)
				break;
			while (true) {
				long start = System.currentTimeMillis();
				if (!findMatches())
					continue;
				long waitPause = paused ? PAUSE
						- (System.currentTimeMillis() - start) : 0;
				if (waitPause > 0)
					sleepWhileInterrupted(waitPause);
				if (!isMatcherChanged())
					break;
			}
			listener.finished();
		}
	}

	private Match find(Matcher matcher, ISearchUnit block, int from) {
		if (matcher.find(from)) {
			int start = matcher.start();
			if (!Character.isLetterOrDigit(block.getText().charAt(start))
					&& prefix)
				start++;
			int end = matcher.end();
			if (end != start) { // don't report 0-length matches
				return new Match(block, start, end - start);
			}
		}
		return null;
	}

	private boolean find(Matcher matcher, ISearchUnit block, int from, int to,
			boolean findFirst) {
		int k = 1;
		int limit = block.getText().length();
		if (from >= to || from > limit)
			return true;
		Match match = find(matcher, block, from);
		if (match != null) {
			from = match.getOffset() + 1;
			if (from >= to || from > limit)
				return true;
			if (findFirst)
				listener.firstFound(match);
			matches.add(match);
			match = find(matcher, block, from);
			while ((match = find(matcher, block, from)) != null) {
				if (match.getOffset() >= to)
					return true;
				matches.add(match);
				if (k++ == 20) {
					if (isMatcherChanged())
						return false;
					k = 0;
				}
				from = match.getOffset() + 1;
			}
		}
		return true;
	}

	private boolean findMatches() {
		boolean isEmpty = pattern == null || pattern.pattern().length() == 0;
		Matcher matcher = isEmpty ? null : pattern.matcher(new String());
		setMatcher(null, prefix, blocks, startIndex, paused);
		if (matcher == null) {
			listener.clear();
			return true;
		}
		matches = new ArrayList<Match>();
		boolean findFirst = true;
		for (int i = 0; i < blocks.length; i++) {
			String text = blocks[i].getText();
			matcher.reset(text);
			int from = startIndex;
			if (!find(matcher, blocks[i], from, text.length(), findFirst))
				return false;
			if (matches.size() > 0)
				findFirst = false;
			List<Match> post = matches;
			matches = new ArrayList<Match>();
			if (!find(matcher, blocks[i], 0, from - 1, findFirst))
				return false;
			if (matches.size() > 0)
				findFirst = false;
			matches.addAll(post);
		}
		if (matches.size() == 0)
			listener.firstFound(null);
		listener.allFound(matches.toArray(new Match[matches.size()]));
		return true;
	}

	public static Pattern createPattern(String pattern,
			boolean isCaseSensitive, boolean isPrefix)
			throws PatternSyntaxException {
		return createPattern(pattern, false, true, isCaseSensitive, isPrefix);
	}

	/**
	 * Creates a pattern element from the pattern string which is either a
	 * reg-ex expression or in our old 'StringMatcher' format.
	 * 
	 * @param pattern
	 *            The search pattern
	 * @param isRegex
	 *            <code>true</code> if the passed string already is a reg-ex
	 *            pattern
	 * @param isStringMatcher
	 *            <code>true</code> if the passed string is in the StringMatcher
	 *            format.
	 * @param isCaseSensitive
	 *            Set to <code>true</code> to create a case insensitive pattern
	 * @param isPrefix
	 *            <code>true</code> to create a pattern that requires a word
	 *            boundary at the beginning and the end.
	 * @return The created pattern
	 * @throws PatternSyntaxException
	 */
	public static Pattern createPattern(String pattern, boolean isRegex,
			boolean isStringMatcher, boolean isCaseSensitive, boolean isPrefix)
			throws PatternSyntaxException {
		if (isRegex) {
			if (isPrefix) {
				StringBuffer buffer = new StringBuffer(pattern.length() + 10);
				buffer.append("\\b(?:").append(pattern).append(")\\b"); //$NON-NLS-1$ //$NON-NLS-2$
				pattern = buffer.toString();
			}
		} else {
			int len = pattern.length();
			StringBuffer buffer = new StringBuffer(len + 10);
			if (len > 0 && isPrefix) {
				buffer.append("(\\A|\\W)");
			}
			appendAsRegEx(isStringMatcher, pattern, buffer);
			pattern = buffer.toString();
		}

		int regexOptions = Pattern.MULTILINE;
		if (!isCaseSensitive) {
			regexOptions |= Pattern.CASE_INSENSITIVE | Pattern.UNICODE_CASE;
		}
		return Pattern.compile(pattern, regexOptions);
	}

	public static StringBuffer appendAsRegEx(boolean isStringMatcher,
			String pattern, StringBuffer buffer) {
		boolean isEscaped = false;
		for (int i = 0; i < pattern.length(); i++) {
			char c = pattern.charAt(i);
			switch (c) {
			// the backslash
			case '\\':
				// the backslash is escape char in string matcher
				if (isStringMatcher && !isEscaped) {
					isEscaped = true;
				} else {
					buffer.append("\\\\"); //$NON-NLS-1$
					isEscaped = false;
				}
				break;
			// characters that need to be escaped in the regex.
			case '(':
			case ')':
			case '{':
			case '}':
			case '.':
			case '[':
			case ']':
			case '$':
			case '^':
			case '+':
			case '|':
				if (isEscaped) {
					buffer.append("\\\\"); //$NON-NLS-1$
					isEscaped = false;
				}
				buffer.append('\\');
				buffer.append(c);
				break;
			case '?':
				if (isStringMatcher && !isEscaped) {
					buffer.append('.');
				} else {
					buffer.append('\\');
					buffer.append(c);
					isEscaped = false;
				}
				break;
			case '*':
				if (isStringMatcher && !isEscaped) {
					buffer.append(".*"); //$NON-NLS-1$
				} else {
					buffer.append('\\');
					buffer.append(c);
					isEscaped = false;
				}
				break;
			default:
				if (isEscaped) {
					buffer.append("\\\\"); //$NON-NLS-1$
					isEscaped = false;
				}
				buffer.append(c);
				break;
			}
		}
		if (isEscaped) {
			buffer.append("\\\\"); //$NON-NLS-1$
			isEscaped = false;
		}
		return buffer;
	}

}
